jni小结 | 您所在的位置:网站首页 › jni doc › jni小结 |
Jni(java native interface)是一种技术,它让java调用其他语言的代码,比如C/C++的代码.在SUN的官方网站上可以下载到相关的文档,看文档总是比较好的,给出链接先: JVM TOOL DOC: http://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html JNI DOC: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
基本原理是将native的代码编译成动态库,在java中加载此动态库,java使用jni调用动态库中的函数,动态库中的函数也可以通过jni调用到java中的函数. 那么要实现这样的功能,需要解决: 1.java中怎样加载动态库 2.Java调用动态库的函数和动态库调用java函数问题 3.参数传递和返回值问题 4.C/C++运行的线程环境问题 5.异常的抛出与捕获问题 6.垃圾回收问题
1.java中怎样加载动态库 Java中加载动态库是调用函数: System.loadLibrary("jnitest"); 这个函数会在项目目录下查找动态库libjnitest.so(linux系统)或jnitest.dll(windows系统),开发android应用程序时,会在项目目录/libs下面查找. 加载时机是在调用库中的函数之前,通常的做法是在类的static域中加载,例如: class JavaTestClass{ static{ System.loadLibrary("jnitest"); } //other method... };
2.Java调用动态库的函数和动态库调用java函数问题 在调用之前,需要完成java函数到动态库函数的一对一的映射才行. 2.1 动态库函数映射到java中 通过这种映射之后,java可以调用到动态库的函数.JVM提供了两种映射方法,静态映射和动态映射.无论静态映射还是动态映射,在java中函数声明都是一样的: private native void jni_api(); 2.1.1静态映射方法: 通常不用这种方法,因为它不好使,不过还是了解一下吧.在定义好java中的函数原型后,就可以使用工具javah通过类文件生成相应的C/C++头文件了. javah -o output.h packetname.classname 将packetname.classname文件中的native函数原型转换成C/C++中的函数原型,结果保存在output.h中.例如: javah -o jnitest.h com.test.JniTest 注意,执行的目录是在包的目录的上一层,不然可能找不到类文件.有了这个头文件,生活就变得容易了,C/C++代码只要包含这个头文件,然后实现函数就OK了. 这个背后的机制其实比较简单,java类中调用native标志的函数时,JVM会在一个映射表中查找映射函数,如果没有函数就按照一定的规则变换此函数原型,然后跟据变换后的原型调用动态中的函数.个人觉得这个方法比较麻烦,主要是函数名比较丑...,简单略过吧,我们主要使用动态映射方法. 2.1.2动态映射方法 上面有提到JVM会查找一个映射表...,我们可以将java中的函数原型和动态库中的函数原型通通注册到那个表中,JVM不就可以顺利找到java的另一半了吗?JVM确实提供了这样的功能.准确地说,从JVM提供了一些操作函数,同时JVM提供一个线程相关的结构体指针估且称为env,env包含了那组操作函数并且提供了包装输出了接口,把env输出的接口称为jni api,我们就是通过这些jni api和JVM打交道的,当然有时候也需要使用JVM本身提供的函数. jni.h中定义了JNI API,来看一下和方法注册相关的: typedef struct { char *name; //函数在java中的名字 char *signature;//函数参数和返回值签名 void *fnPtr; //native代码中的函数指针 } JNINativeMethod; //注册到映射表 jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods); jint UnregisterNatives(jclass clazz);//取消注册 clazz是java中声明的native函数所在的类的类型对像,注意不是类的实例,methods是映射结构体指针,nMethods是映射结构体的个数.通常的做法是先定义一个JNINativeMethod类型的数组并初始化,然后使用RegisterNatives一次性注册. 看起来非常简单,但是有个问题,jclass怎么来的呢,还有那个签名怎么回事?先来看jclass: jclass FindClass(const char *name); 参数name是java中类的名字,是全名,包含包的名字. 对于FindClass和RegisterNatives等许多的jni函数,你会看到别人的代码都是这么用的: 在C里面: (*env)->FindClass(env, "java/lang/String"); 在C++中: env->FindClass("java/lang/String"); 注意,所有的jni函数都是这么用的 签名是为了确定java中函数的参数和返回值,毕境上面只给出了name,怎么得到签名呢,一个是自己手动去写,这个需要熟悉类型到签名的转换,另一个就是使用工具javap,来看个例子: javap -s com.test.JniTest public void play(java.lang.String); Signature: (Ljava/lang/String;)V 很简单吧,不需要去记那么多规则,好吧,那么动态注册映射的时机呢,记得java函数中是在static中加载了动态库,那动态库可以在哪加载呢? /* Defined by native libraries. */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved); JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved); 没错,就是他们,一个在加载后调用,一个在卸载后调用,动态库跟据自己的需要实现这两个函数.在加载动态库后,JVM会去查找动态库有没有提供JNI_OnLoad函数,如果没有JVM会抱怨一声然后正常加载,如果有就会调用它.参数vm是JVM的指针,可以从中获得env,从而干许多事.不管干了什么,这个函数最后要返回JNI的版本值,比如: #define JNI_VERSION_1_1 0x00010001 #define JNI_VERSION_1_2 0x00010002 #define JNI_VERSION_1_4 0x00010004 #define JNI_VERSION_1_6 0x00010006 不返回的话,也没什么问题,只是JVM会报个错,然后整个进程都不好了. 2.2 java中的函数映射到动态库中 通过这种映射之后,动态库可以调用到java的函数.这个没有什么静态映射的方法了,是通过查找到java函数的句柄,然后使用jni api来使这个函数执行.查找句柄的jni api: jmethodID GetMethodID(jclass clazz, const char *name,const char *sig); 其中clazz是通过FindClass返回的,name是java中函数的名字,sig是java函数的签名.找到这个句柄之后,就可以调用jni api来执行这个函数了,env根据返回值的不同提供了许多函数,比如: jobject CallObjectMethod(jobject obj, jmethodID methodID, ...); jboolean CallBooleanMethod(jobject obj,jmethodID methodID, ...); void CallVoidMethod(jobject obj, jmethodID methodID, ...); ... 除了这些之外,还因为参数提供方法不一样分成了V和A版本: void CallVoidMethodV(jobject obj, jmethodID methodID,va_list args); void CallVoidMethodA(jobject obj, jmethodID methodID,const jvalue * args); 动态库除了可以访问java函数之外,还可以访问成员变量,同样是先取得句柄,然后调用jni api执行: jfieldID GetFieldID(jclass clazz, const char *name,const char *sig); jobject GetObjectField(jobject obj, jfieldID fieldID); void SetObjectField(jobject obj, jfieldID fieldID, jobject val); .... 很多动态库保存全局结构体的方法是,在java中定义一个int的成员变量,然后在native代码中申请到空间后,写入到此成员变量,这样当java调用其他的函数时,在函数中就可以通过env获取到全局结构体的指针了. 3.参数传递和返回值问题 看到上面的jnit,jobject是不是觉得头晕,这得来介绍下java中的类型和jni中的类型,以及C/C++的类型: java wide(byte) jni wide(byte) C/C++ byte 1 jbyte 1 char int 4 jint 4 int long 8 jlong 8 long long char 2 jchar 2 unsigned short boolean 1 jboolean 1 unsigned char short 2 jshort 2 short float 4 jfloat 4 float double 8 jdouble 8 double 这是基本类型的对应表,这是啥意思呢,其实就是java中一个类型,比如char到了jni api时会转换成jchar,而jchar在C/C++中其实是unsigned short类型.其中黄色标记的类型长度是操作系统依赖的,这里写的都是linux. 除了基本类型的转换之外,还有数组和对像的类型. java jni C/C++ object jobject 类的指针 java.lang.Class jclass java.lang.Throwable jthrowable java.lang.String jstring boolean[] jbooleanArray byte[] jbyteArray char[] jcharArray short[] jshortArray int[] jintArray long[] jlongArray float[] jfloatArray double[] jdoubleArray object[] jobjectArray 对于这种非基本类型的类型,在C/C++中访问的时候需要依靠env的帮助,跟据类型不同,这些jni api也不一样: // 创建一个jbyteArray jbyteArray NewByteArray(jsize len) //从jbyteArray获取jbyte数组指针 jbyte * GetByteArrayElements(jbyteArray array, jboolean *isCopy); //释放数组空间 void ReleaseByteArrayElements(jbyteArray array,jbyte *elems,jint mode); ...... 参数isCopy用来指示在创建jbyte数组的时候是否进行了复制,如果进行了复制,*isCopy=JNI_TRUE,如果没有进行复制返回的是jbyteArray本身的数据地址那么*isCopy=JNI_FALSE.可以传入0作为参数,忽略其返回值.参数mode指示这个函数的操作方式: Primitive Array Release Modes mode actions 0 copy back the content and free the elems buffer JNI_COMMIT copy back the content but do not free the elems buffer JNI_ABORT free the buffer without copying back the possible changes
4.C/C++运行的线程环境问题 java中可以有许多线程,当在这些线程中调用到动态库的函数时,其环境信息也会传递下来,JVM提供了一个结构体env来保存这些信息供动态库使用.怎么获得这个结构体呢,普通的被调用函数不用说了,参数中就有env结构体指针.要自己获取env结构体的地方有两个,一个是在JNI_Onload中,一个是在动态库自己创建的线程中. 在JNI_Onload中,可以通过JVM的接口获取: jint GetEnv(JavaVM *vm, void **env, jint version); 在自已创建的函数中: jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args); jint DetachCurrentThread(JavaVM *vm); 或许有人会比较奇怪,为什么在自创线程中不能调用GetEnv得到env呢,其实是因为所有的线程都必须先AttachCurrentThread之后才能调用GetEnv,而java中创建的线程都调用了AttachCurrentThread,这个应该是在API中调用好了的. java 创建一个JVM进程,JVM创建若干线程,所以JVM和线程无关,所有线程中共享一个JVM,我们可以保存起来使用,而env则和线程有关,必须AttachCurrentThread之后才能使用,用完后也必须释放掉. 5.异常的抛出与捕获问题 C/C++可以抛出异常给java代码: jint Throw(jthrowable obj); jint ThrowNew(jclass clazz,const char *message);//clazz应该是FindClass("java/lang/Throwable")返回的,message是给异常携带的错误信息. C/C++可以捕获java抛出的异常: jthrowable ExceptionOccurred(JNIEnv *env);//如果没有异常返回NULL,否则返回异常. void ExceptionDescribe(JNIEnv *env);//打印当前异常信息 void ExceptionClear(JNIEnv *env);//清除当前异常信息
6.垃圾回收问题 JVM会不定时回收"垃圾",所谓的"垃圾"是指引用为0的对像,在java中每次赋值后都会增加左值的引用减少右值的引用,但是在C/C++中不会,所以这会引起问题,试想,java将一个对像传递给了native层代码,然后这个对像在java中引用减少为0了,那么它就会当成垃圾回收掉,这么可怕的事情怎么避免呢,当然是在native代码中也增加一次引用了. jni中一共提供了三种引用类型,全局引用GlobalRef,本地引用LocalRef,弱全局引用WeadGlobalRef. 全局引用只能手动释放,不释放其对像会永久存在,除非进程被清理,它的创建与释放: jobject NewGlobalRef(jobject obj); void DeleteGlobalRef(jobject globalRef); 本地引用包括传为参数传进来的jobject和在函数中定义的jobject,本地引用会在函数返回时自动释放,也可以自己手动释放: jobject NewLocalRef(JNIEnv *env, jobject ref); void DeleteLocalRef(jobject localRef); 弱全局引用指向的对像可能被垃圾回收: jweak NewWeakGlobalRef(JNIEnv *env, jobject obj); void DeleteWeakGlobalRef(JNIEnv *env, jweak obj); IsSameObject(jweak,NULL);//判断弱全局引用指向的对像是否被回收
注意一点,垃圾回收的时间没法确定,如果想快速释放一个对像,还是需要调用java层进行主动释放空间.
7.一个简单的例子: com.test.JniTest.java: package com.test; public class JniTest { static{ System.loadLibrary("jnitest"); } public native static void jni_api(); public static void main(String args[]){ try{ jni_api(); }catch(Throwable e){ System.out.println("------------------"); System.out.println(e.getMessage()); } } }
jni/jnitest.h(使用命令javah -o jnitest.h com.test.JniTest生成 /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class com_test_JniTest */
#ifndef _Included_com_test_JniTest #define _Included_com_test_JniTest #ifdef __cplusplus extern "C" { #endif /* * Class: com_test_JniTest * Method: jni_api * Signature: ()V */ JNIEXPORT void JNICALL Java_com_test_JniTest_jni_1api (JNIEnv *, jobject);
#ifdef __cplusplus } #endif #endif
jni/jnitest.cpp: #include #include "jnitest.h" #include
JNIEXPORT void JNICALL Java_com_test_JniTest_jni_1api (JNIEnv *env, jobject thiz){ printf("----jni test---\n"); jclass c=env->FindClass("java/lang/Throwable"); printf("----find class 0x%x---\n",c); if(c) env->ThrowNew(c,"hello,this is exception"); else printf("can not find class!\n"); }
编译生成动态库: gcc -shared -o ../libjnitest.so jnitest.cpp -I/usr/local/jdk1.7.0_45/include -I/usr/local/jdk1.7.0_45/include/linux 测试运行: ------------------ hello,this is exception ----jni test--- ----find class 0x8f0821a8--- |
CopyRight 2018-2019 实验室设备网 版权所有 |